﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using mshtml;
using System.Net;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Xml.Linq;

namespace jTest
{
    /// <summary>
    /// A combination of static creation methods.
    /// </summary>
    public static class HtmlDocumentHelper
    {
    // Methods
        /// <summary>
        /// Download a document and convert it to an HtmlDocument.
        /// </summary>
        /// <param name="source">The web address of the the file.</param>
        /// <returns>A <see cref="T:HTMLDocument"/> that is parsed from the website.</returns>
        public static HTMLDocument DownloadDocument(Uri source, string PostData, bool GZip)
        {
            if (source == null)
                throw new ArgumentNullException("source", "You must specify a source url.");

            if (source.IsFile)
            {
                String fileContents = File.ReadAllText(source.LocalPath);
                return HtmlDocumentHelper.LoadDocument(fileContents);
            }

            HttpWebRequest Http = (HttpWebRequest)WebRequest.Create(source);
            Http.UserAgent = "jScraper/1.0.0.0";
            if (GZip)
            {
                Http.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
            }

            if (!string.IsNullOrEmpty(PostData))
            {
                Http.Method = "POST";
                byte[] lbPostBuffer = Encoding.Default.GetBytes(PostData);

                Http.ContentLength = lbPostBuffer.Length;

                Stream PostStream = Http.GetRequestStream();
                PostStream.Write(lbPostBuffer, 0, lbPostBuffer.Length);
                PostStream.Close();
            }

            HttpWebResponse webResponse = (HttpWebResponse)Http.GetResponse();

            Stream responseStream = responseStream = webResponse.GetResponseStream();
            if (webResponse.ContentEncoding.ToLower().Contains("gzip"))
                responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
            else if (webResponse.ContentEncoding.ToLower().Contains("deflate"))
                responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);

            StreamReader Reader = new StreamReader(responseStream, Encoding.Default);
            string html = Reader.ReadToEnd();

            HTMLDocument resultDocument = new HTMLDocument();
            IHTMLDocument2 document2 = (IHTMLDocument2)resultDocument;
            document2.write(html);

            webResponse.Close();
            responseStream.Close();
            responseStream.Dispose();
            document2.close();

            return resultDocument;
        }

        /// <summary>
        /// Pause for some randome time out.
        /// </summary>
        public static void RandomTimeOut()
        {
            Thread.Sleep(new Random().Next(0, 1000));
        }

        /// <summary>
        /// Convert html an HtmlDocument.
        /// </summary>
        /// <param name="source">An html string.</param>
        /// <returns>A <see cref="T:HTMLDocument"/> that is parsed from the website.</returns>
        public static HTMLDocument LoadDocument(String html)
        {
            HTMLDocument resultDocument = new HTMLDocument();            
            IHTMLDocument2 document2 = (IHTMLDocument2)resultDocument;

            document2.write(html);
            document2.close();

            return resultDocument;
        }

        /// <summary>
        /// Return all the text in the document including the html document.
        /// </summary>
        /// <remarks>
        /// This may seem like overboard but it was pretty easy and since
        /// since this is primarly used to load IE I know that it can be sensitive.
        /// </remarks>
        /// <param name="document">Any html document.</param>
        /// <returns>All of the text in html document.</returns>
        public static String GetAllHtml(this HTMLDocument document)
        {
            IHTMLCommentElement documentComment = document.firstChild as IHTMLCommentElement;
            String documentText = document.documentElement.outerHTML;
            if (documentComment != null)
            {
                documentText = documentComment.text + documentText; 
            }

            return documentText;
        }
        
        /// <summary>
        /// Get the inner html using the jquery selector;
        /// </summary>
        /// <param name="selector"></param>
        /// <returns></returns>
        public static String GetInnerHtml(this HTMLDocument document, String selector)
        {
            // TODO: Detect if the selector starts with $ or Jquery and change the execScript to allow a true jquery selector
            // Check the state of the document object.            
            if (document.body == null)
                throw new InvalidOperationException("The body tag cannot be null.");

            mshtml.IHTMLWindow2 window = document.parentWindow;
            if (window == null)
                throw new InvalidOperationException("The page window tag cannot be null.");

            // Load JQuery 
            // TODO: Load JQuery only one time.
            var scriptElement = document.createElement("script");
            scriptElement.setAttribute("type", "text/javascript");
            // TODO: Load jquery url from a config file.
            scriptElement.setAttribute("src", "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js");
            mshtml.IHTMLDOMNode scriptNode = (mshtml.IHTMLDOMNode)scriptElement;
            document.appendChild(scriptNode);

            IHTMLElement safeElement = document.createElement("div");
            safeElement.id = Guid.NewGuid().ToString();
            mshtml.IHTMLDOMNode safeNode = (mshtml.IHTMLDOMNode)safeElement;
            document.appendChild(safeNode);

            String appendSafeElementJavascript = String.Format(" $('#{0}').append( $('{1}') ); ", safeElement.id, selector);

            window.execScript(appendSafeElementJavascript, "JScript");

            return safeElement.innerHTML;
        }

        /// <summary>
        /// Referance a script file referance such as jquery.
        /// </summary>
        /// <param name="path">Location of the script file.</param>        
        public static void ReferenceJavaScriptFile(this HTMLDocument document, Uri path)
        {
            // TODO: Do I need to add a local check to make the path = file://c://temp//test.htm
            IHTMLElement scriptElement = document.createElement("script");
            scriptElement.setAttribute("type", "text/javascript");
            scriptElement.setAttribute("src", path.AbsoluteUri);            
            
            mshtml.IHTMLDOMNode script = (mshtml.IHTMLDOMNode)scriptElement;
            document.appendChild(script);
        }

        /// <summary>
        /// Load a script from a string.
        /// </summary>
        /// <param name="script">The JScript to load.</param>
        public static void LoadScript(this HTMLDocument document, String script)
        {        
            IHTMLElement scriptElement = document.createElement("script");
            IHTMLScriptElement javascriptElement = (IHTMLScriptElement)scriptElement;
             
            javascriptElement.text = script;
            mshtml.IHTMLDOMNode scriptNode = (mshtml.IHTMLDOMNode)javascriptElement;

            document.appendChild(scriptNode);
        }

        /// <summary>
        /// Add a referance a css file.
        /// </summary>
        /// <param name="path">Location of the script file.</param>        
        public static void ReferanceStyleSheet(this HTMLDocument document, Uri path)
        {
            mshtml.IHTMLStyleSheet css = (mshtml.IHTMLStyleSheet)document.createStyleSheet(path.AbsoluteUri, 0);            
        }

        /// <summary>
        /// Add page level styles.
        /// </summary>
        /// <param name="styles">The css styles to add to the page.</param>
        public static void AddPageStyles(this HTMLDocument document, String styles)
        {
            mshtml.IHTMLStyleSheet css = (mshtml.IHTMLStyleSheet)document.createStyleSheet("", 0);
            css.cssText = styles;  
        }
        
        /// <summary>
        /// Return a collection of child elements.
        /// </summary>
        /// <param name="element">The element to collect children.</param>
        /// <returns>A <see cref="T:Collection&lt;IHTMLElement&gt;"/></returns>
        public static IEnumerable<IHTMLElement> Children(this IHTMLElement element) 
        {
            if (element == null)
                throw new ArgumentNullException("element", "Element cannot be null");

            IHTMLElement htmlElement = (IHTMLElement)element;
            IHTMLElementCollection childElements = (IHTMLElementCollection)htmlElement.children;
            List<IHTMLElement> results = new List<IHTMLElement>();
            foreach (IHTMLElement item in childElements)
            {
                results.Add(item);
            }

            return results;
        }

        /// <summary>
        /// Return a collection of all child elements.
        /// </summary>
        /// <param name="element">The element to collect children.</param>
        /// <returns>A <see cref="T:Collection&lt;IHTMLElement&gt;"/></returns>
        public static IEnumerable<IHTMLElement> All(this IHTMLElement element)
        {
            if (element == null)
                throw new ArgumentNullException("element", "Element cannot be null");

            IHTMLElement htmlElement = (IHTMLElement)element;
            IHTMLElementCollection childElements = (IHTMLElementCollection)htmlElement.all;
            List<IHTMLElement> results = new List<IHTMLElement>();
            foreach (IHTMLElement item in childElements)
            {
                results.Add(item);
            }

            return results;
        }

        /// <summary>
        /// Return a collection of child elements.
        /// </summary>
        /// <param name="element">The element to collect children.</param>
        /// <returns>A <see cref="T:Collection&lt;IHTMLElement&gt;"/></returns>
        public static Dictionary<String, String> Attributtes(this IHTMLElement element)
        {
			if (element == null)
				throw new ArgumentNullException("element", "Element cannot be null");

			IHTMLDOMNode currentNode = (IHTMLDOMNode)element;
			IHTMLAttributeCollection attributes = (IHTMLAttributeCollection )currentNode.attributes;

			Dictionary<String, String> results = new Dictionary<String, String>();
			foreach (IHTMLDOMAttribute2 item in attributes)
			{
				results.Add(item.name, item.value);
			}

			return results;
        }

		///// <summary>
		///// Convert the element into a <see cref="System.Xml.Linq"/>.
		///// </summary>
		///// <param name="element">The element to convert.</param>
		///// <returns>Returns the a <see cref="System.Xml.Linq"/> else returns null.</returns>
		//public static XElement ToXElement(this IHTMLElement element)
		//{
		//    throw new NotImplementedException();
		//    string resultString = "<DIV class='nestedContainer'><H3>Nested Item</H3><P>item text</P></DIV>";
		//        //element.outerHTML.Trim();
		//    return XElement.Parse(resultString);
		//}
    }
}
